#!/usr/bin/env python2

import os
import sys
import gzip
import re
import socket
import time
import subprocess
import fcntl
import types
import errno
import errno
import getopt
import copy
import random
from utils import _get_value, Exp, _exec_pipe, _exec_shell, _str2dict, _dwarn, _dmsg, _derror, _set_value
from config import Config
from localfs import LocalFs
from storage import Storage
from movedata import Object, Replica, Node, Location, Rack
from chunkbalance import ChunkBalance

max_retry = 1000


class Instence(object):
    def __init__(self, config):
        home = os.path.abspath(config.home + "/data")

        if not os.path.exists(home):
            os.system('mkdir -p %s' % home)
        self.disk_status = 0
        self.config = config

        self.lich_admin = config.lich + "/libexec/lich.admin"
        self.lich_inspect = config.lich + "/libexec/lich.inspect"

        if (os.path.exists("/usr/bin/numactl")):
            self.lichd = "/usr/bin/numactl --interleave all " + config.lich + "/sbin/lichd"
        else:
            self.lichd = config.lich + "/sbin/lichd"

        self.iscsigw_workdir = "/tmp/lichgw/status"
        self.iscsigw_status_path = self.iscsigw_workdir + "/status"
        self.lichd_status_path = home + "/status/status"
        self.iscsigw = config.lich + "/sbin/lichgw"

        self.cmd = self.lichd + " --home %s" % (home)

        self.home = home
        self.pid = -1
        self.ppid = -1
        self.deleting = False
        self.deleted = False
        self.skiped = False
        self.nomount = False

        try:
            if (self.running() or self.starting()):
                self.pid = int(_get_value(self.home + "/status/status.pid"))
                self.ppid = int(_get_value(self.home + "/status/parent.pid"))
        except:
            pass

        self.__getname()

        try:
            tmp = home + '/check_' + str(random.random())
            _set_value(tmp, "test")
            if os.path.exists(tmp):
                os.unlink(tmp)
        except:
            self.disk_status = errno.EIO
            if os.path.exists(tmp):
                os.unlink(tmp)
            return

        if (not os.path.exists(self.home + "/fake")):
            if (os.stat(home).st_dev == os.stat(home + "/..").st_dev):
                pass
                #self.nomount = True

        if (os.path.exists(self.home + "/deleting")):
            self.deleting = True

        if (os.path.exists(self.home + "/deleted")):
            self.deleted = True

        if (os.path.exists(self.home + "/skip")):
            self.skiped = True

    def __getname(self):
        name = os.path.split(self.home)
        try:
            self.name = _get_value(self.home + "/node/config/name") 
        except:
            self.name = self.config.hostname
            return

        if (self.name[-1] == '\n'):
            self.name = self.name[0:-1]

        if (self.name == "none"):
            self.name = self.config.hostname

    def __getpid(self, ttyonly=False):
        i = 0
        while True:
            global max_retry
            if (i > max_retry):
                raise Exp(errno.EIO, ' * %s [fail]' % (self.cmd))
            if (self.running() or self.starting()):
                while (1):
                    a = _get_value(self.home + "/status/status.pid")
                    if (a != ''):
                        self.pid = int(a)
                        break
                    else:
                        time.sleep(0.1)

                while (1):
                    a = _get_value(self.home + "/status/parent.pid")
                    if (a != ''):
                        self.ppid = int(a)
                        break
                    else:
                        time.sleep(0.1)

                break
            else:
                time.sleep(0.1)
                i = i + 1

    def __wait(self, ttyonly=False):
        i = 0
        while True:
            global max_retry
            if (i > max_retry):
                raise Exp(errno.EIO, ' * %s [wait fail]' % (self.cmd))

            if self.running():
                break
            else:
                time.sleep(0.1)
                i = i + 1

    def start(self, ttyonly=False):
        if (self.disk_status):
            _derror(' * %s [disk error]' % (self.cmd), ttyonly)
            return False

        if (self.nomount):
            _derror(' * %s [no mount]' % (self.cmd), ttyonly)
            return False

        if (self.deleted):
            _derror(' * %s [deleted]' % (self.cmd), ttyonly)
            return False

        if (self.skiped):
            _derror(' * %s [skiped]' % (self.cmd), ttyonly)
            return False

        if (self.running()):
            _dwarn(' * %s [running]' % (self.cmd), ttyonly)
            return False

        if (self.starting()):
            _dwarn(' * %s [starting]' % (self.cmd), ttyonly)
            return False

        path = self.home + "/node/config/clustername"
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                _derror(' * %s [no init]' % (self.cmd), ttyonly)
                return False

        subprocess.call(self.cmd, shell=True)

        try:
            self.__getpid(ttyonly)
            self.__wait(ttyonly)
        except Exp, e:
            _dwarn('%s' % (e.err), ttyonly)
            return e.errno

        subprocess.call(self.iscsigw, shell=True)

        _dmsg(' * %s [ok]' % (self.cmd), ttyonly)

    def stop(self, ttyonly=False):
        if (self.nomount):
            _derror(' * %s [no mount]' % (self.cmd), ttyonly)
            return False

        if (self.running() == False and self.starting() == False):
            _derror("%s already stopped" % self.cmd, ttyonly)
            return 0

        try:
            self.__getpid(ttyonly)
        except Exp, e:
            _dwarn('%s' % (e.err), ttyonly)
            return e.errno

        #os.system("kill -USR2 %u" % self.ppid)
        #os.system("kill -USR2 %u" % self.pid)

        _dmsg("stop %s %s,%s" % (self.home, self.ppid, self.pid), ttyonly)
        #temporary solution for bug #2096
        os.system("kill -USR2 %u %u" % (self.ppid, self.pid))
        #os.system("kill -9 %u" % self.pid)

        if os.path.exists(os.path.join(self.iscsigw_workdir, 'status')):
            status = _get_value(os.path.join(self.iscsigw_workdir, 'status'))
            if status.startswith('running'):
                gwppid = int(_get_value(os.path.join(self.iscsigw_workdir, 'parent.pid')))
                gwpid = int(_get_value(os.path.join(self.iscsigw_workdir, 'status.pid')))
                os.system("kill -USR2 %u %u" % (gwppid, gwpid))

 
        time.sleep(0.1)

        if (self.running() or self.starting()):
            time.sleep(0.1)
            #_dwarn("%s, still running, sleep 1" % (self.name), ttyonly)
            time.sleep(1)
            if (self.running() or self.starting()):
                _derror ("stop %s cmd '%s' pid %u/%u" % (self.name, self.cmd, self.ppid, self.pid), ttyonly)
                try:
                    self.__getpid(ttyonly)
                    os.system("kill -9 %u %u" % (self.ppid, self.pid))
                except Exp, e:
                    _dwarn('%s' % (e.err), ttyonly)
                    return e.errno

        i = 0
        while True:
            if (i > max_retry):
                _derror("stop instence %u fail" %(i), ttyonly)
                return errno.EIO
            if (self.running() == False and self.starting() == False):
                break
            else:
                time.sleep(0.01)
                i = i + 1

    def child_running(self):
        path = self.home + "/status/status.pid"
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                return False

        try:
            fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as err:
            if err.errno != errno.EAGAIN:
                raise
            else:
                return True

        fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
        fd.close()
        return False

    def running(self):
        if (self.disk_status):
            return False

        path = self.lichd_status_path
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                return False

        try:
            fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as err:
            if err.errno != errno.EAGAIN:
                raise
            else:
                buf = _get_value(path)
                if (buf == "running\n"):
                    if (self.child_running()):
                        return True
                    else:
                        return False
                else:
                    return False

        fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
        fd.close()
        return False

    def starting(self):
        if (self.disk_status):
            return False

        path = self.lichd_status_path
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                return False

        try:
            fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as err:
            if err.errno != errno.EAGAIN:
                raise
            else:
                buf = _get_value(path)
                if (buf == "starting\n"):
                    if (self.child_running()):
                        return True
                    else:
                        return False
                else:
                    return False

        fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
        fd.close()
        return False

    def gw_running(self):
        if (self.disk_status):
            return False

        path = self.iscsigw_status_path
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                return False

        try:
            fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as err:
            if err.errno != errno.EAGAIN:
                raise
            else:
                buf = _get_value(path)
                if (buf == "running\n"):
                    if (self.child_running()):
                        return True
                    else:
                        return False
                else:
                    return False

        fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
        fd.close()
        return False

    def gw_starting(self):
        if (self.disk_status):
            return False

        path = self.iscsigw_status_path
        try:
            fd = open(path, 'r')
        except IOError as err:
            if err.errno != errno.ENOENT:
                raise
            else:
                return False

        try:
            fcntl.flock(fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        except IOError as err:
            if err.errno != errno.EAGAIN:
                raise
            else:
                buf = _get_value(path)
                if (buf == "starting\n"):
                    if (self.child_running()):
                        return True
                    else:
                        return False
                else:
                    return False

        fcntl.flock(fd.fileno(), fcntl.LOCK_UN)
        fd.close()
        return False

    def quorum_size(self):
        path = self.home + "/node/paxos/quorum"
        st = os.stat(path)
        #print("%s size %u" % (path, st.st_size))
        return st.st_size

    def _show_offline(self):
        return {"hostname": self.name,
                "running": False,
                "starting": False,
                'deleting': None,
                "status": None,
                'ready': None,
                'skiped': False,
                "writeable": None,

                "cluster": None,
                "nid": None,
                "capacity": None,
                "used": None,
                "admin": None,
                "uptime": 0,
                "metas": None,
                "load": 0,
                "gateway": None}

    def _show_deleted(self):
        return {"hostname": self.name,
                "running": False,
                "starting": False,
                "status": 'deleted',
                'deleting': None,
                "writeable": None,
                'skiped': False,
                'ready': None,
                "cluster": None,
                "nid": None,
                "capacity": None,
                "used": None,
                "admin": None,
                "uptime": 0,
                "metas": None,
                "load": 0,
                "gateway": None}

    def show(self):
        stat = self._show_offline()
        newstat = None

        if self.deleted or self.disk_status:
            return self._show_deleted()

        running = self.running()
        starting = self.starting()
        if (running or starting):
            try:
                res = _exec_pipe([self.lich_admin, '--stat', self.name], 3, False)
            except Exp, e:
                res = {}

            if (len(res) == 0):
                running = False
                starting = False
                stat = self._show_offline()
            else:
                newstat = _str2dict(res)
                if (newstat['admin'] == newstat['hostname']):
                    newstat['status'] = 'admin'

        if (newstat):
            for (k, v) in stat.items():
                if (k == "running"):
                    stat[k] = running
                    continue
                if (k == "starting"):
                    stat[k] = starting
                    continue

                if (k in newstat):
                    stat[k] = newstat[k]
                else:
                    stat.pop(k)

            for k in newstat:
                if k == 'pool_count':
                    stat[k] = newstat[k]
                elif k.endswith('.total'):
                    pool = k.split('.')[0]
                    if 'pool' not in stat:
                        stat['pool'] = {}
                    if pool not in stat['pool']:
                        stat['pool'][pool] = {}
                    stat['pool'][pool]['total'] = newstat[k]
                elif k.endswith('.used'):
                    pool = k.split('.')[0]
                    if 'pool' not in stat:
                        stat['pool'] = {}
                    if pool not in stat['pool']:
                        stat['pool'][pool] = {}
                    stat['pool'][pool]['used'] = newstat[k]


        stat['load'] = "%f" % (float(stat['load']) / 1000 / 1000)

        if (stat['hostname'] == "none"):
            stat['hostname'] = self.name

        stat['skiped'] = self.skiped

        iscsigw_running = self.gw_running()
        iscsigw_starting = self.gw_starting()
        if (iscsigw_running or iscsigw_starting):
            stat['gateway'] = 'on'
        else:
            stat['gateway'] = 'off'

        return stat

    def init(self, clustername, service = -1, ttyonly=False):
        if (os.path.exists(self.home + "/node")):
            _derror(' * %s [inited]' % (self.cmd), ttyonly)
            return False

        #_dmsg("service %d" % (self.nomount))
        if (self.nomount):
            _derror(' * %s [no mount]' % (self.cmd), ttyonly)
            return False

        cmd = self.lichd + " --init --clustername %s --home %s" %(clustername, self.home)

        _dmsg(cmd)
        ret = subprocess.call(cmd, shell=True)
        if (ret != 0):
            if (ret == errno.EEXIST):
                raise Exp(ret, "%s already initted" %(self.home))
            else:
                raise Exp(ret, "init %s failed, ret %d" %(self.home, ret))

        self.__getname()

    def isadmin(self):
        res = _exec_pipe([self.lich_admin, '--stat', self.name])
        res = _str2dict(res)
        admin = res['admin']

        if (admin == self.name):
            #_dmsg("is admin")
            return True
        else:
            #_derror("local name '%s' admin '%s'" % (self.name, admin))
            return False

    def skip(self, s):
        if (s == '0'):
            os.system("rm -rf %s/skip" % (self.home))
            self.skiped = False
            self.start()
        elif (s == '1'):
            os.system("touch %s/skip" % (self.home))
            self.stop()

    def __log_file(self):
        logfile = os.path.abspath(self.config.home + "/log/lich.log")
        return logfile

    def log_backup(self, stime):
        logfile = self.__log_file()
        dist = logfile + "-" + stime
        print ("copy %s to %s" % (logfile, dist))
        _exec_pipe(["cp", logfile,  dist], 0, False);

    def log_clean(self):
        logfile = self.__log_file()
        print ("clean %s" % (logfile))
        os.system("echo "" > " + logfile)

    def log_tail(self):
        logfile = self.__log_file()
        res = "tail %s @ %s:\n" % (logfile, self.name)
        return res + _exec_pipe(["tail", logfile], 0, False);

    def log_collect(self, begin, end):
        collect_time = str(int(time.time()))
        collect_log_dir = '/tmp'
        collect_log = '/tmp/collect-' + self.name + '-log' + '-' + collect_time
        collect_log_gz = '/tmp/collect-' + self.name + '-log' + '-' + collect_time + '.gz'

        c = open(collect_log, 'a')

        ### collect /opt/fusionstack/log/backup/lich.log.xxxx.gz
        travelfiles = []
        backupdir = '/opt/fusionstack/log/backup'
        for parent, dirnames, backupfiles in os.walk(backupdir):
            for backupfile in backupfiles:
                if 'lich.log' in backupfile and '.swp' not in backupfile:
                    if re.match(r'lich.log.\d+.gz', backupfile):
                        backupfile = os.path.join(parent, backupfile)
                        travelfiles.append(backupfile)

        fileSort = {}
        etime = []
        for item in travelfiles:
            if '.swp' in item:
                continue
            fileSort[int(item.split('.')[2])] = item

        etime = sorted(fileSort.iteritems(), key=lambda d:d[1], reverse=False)

        firstline = True
        for item in etime:
            firstline = True
            print ("collecting %s" % (item[1]))
            try:
                f = gzip.open(fileSort[item[0]], 'r')
                line = f.readline()
                while line:
                    m = re.match('\d+-\d+-\d+\s\d+:\d+:\d+\/(\d+)', line)
                    if m:
                        if firstline:
                            if float(m.group(1)) > end:
                                break
                            else:
                                firstline = False
                        if float(m.group(1)) >= begin and float(m.group(1)) <= end:
                            c.write(line)
                    line = f.readline()
            except Exception, e:
                pass
            finally:
                f.close()

        ### collect /opt/fusionstack/log/lich.log-xxx-xxx
        travelfiles = []
        backupdir = '/opt/fusionstack/log/'
        for parent, dirnames, backupfiles in os.walk(backupdir):
            for backupfile in backupfiles:
                if 'lich.log' in backupfile and 'backup' not in parent and '.swp' not in parent:
                    if re.match(r'lich.log-\d+-\d+', backupfile):
                        backupfile = os.path.join(parent, backupfile)
                        travelfiles.append(backupfile)

        fileSort = {}
        etime = []
        for item in travelfiles:
            if item == '/opt/fusionstack/log/lich.log' or '.swp' in item:
                continue

            fileSort[int(item.split('-')[1] + item.split('-')[-1])] = item

        etime = sorted(fileSort.iteritems(),key=lambda d:d[1],reverse=False)

        firstline = True
        for item in etime:
            print ("collecting %s" % (item[1]))
            try:
                f = open(fileSort[item[0]], 'r')
                line = f.readline()
                while line:
                    m = re.match('\d+-\d+-\d+\s\d+:\d+:\d+\/(\d+)', line)
                    if m:
                        if firstline:
                            if float(m.group(1)) > end:
                                break
                            else:
                                firstline = False
                        if float(m.group(1)) >= begin and float(m.group(1)) <= end:
                            c.write(line)
                    line = f.readline()
            except Exception, e:
                pass
            finally:
                f.close()

        ### collect /opt/fusionstack/log/lich.log
        logfile = self.__log_file()
        print ("collecting %s" % (logfile))
        try:
            f = open(logfile, 'r')
            line = f.readline()
            while line:
                m = re.match('\d+-\d+-\d+\s\d+:\d+:\d+\/(\d+)', line)
                if m:
                    if float(m.group(1)) >= begin and float(m.group(1)) <= end:
                        c.write(line)
                line = f.readline()
        except Exception, e:
            pass
        finally:
            f.close()

        c.close()
        try:
            os.system("cd %s && gzip -v %s" % (collect_log_dir, collect_log))
        except Exception, e:
            os.system("rm -rf %s" % collect_log_gz)
            raise

        os.system("mv %s /root" % collect_log_gz)

    def chunkbalance(self, t):
        chunkbalance = ChunkBalance(self.config, self.home, self.name)
        chunkbalance.balance(t)


def usage():
    print ("usage:")
    print (sys.argv[0] + " --init <path>")
    print (sys.argv[0] + " --start <path>")
    print (sys.argv[0] + " --stop <path>")
    print (sys.argv[0] + " --stat <path>")
    print (sys.argv[0] + " --disable <path>")
    print (sys.argv[0] + " --enable <path>")


def main():
    verbose = 0
    op = ""
    ext = None
    try:
        opts, args = getopt.getopt(
                sys.argv[1:], 
                'hv', ['drop=', 'start=', 'stop=', 'init=', 'stat=', 'disable=', 'enable=']
                )
    except getopt.GetoptError, err:
        print str(err)
        usage()
        exit(1)

    for o, a in opts:
        if o in ('--help'):
            usage()
            exit(0)
        elif o in ('--drop', '--start', '--stop', '--init', '--stat', '--disable', '--enable'):
            op = o
            ext = a
        else:
            assert False, 'oops, unhandled option: %s, -h for help' % o
            exit(1)

    config = Config()
    instence = Instence(config)

    if (op == '--init'):
        try:
            instence.init(config.clustername)
        except Exp, e:
            if (e.errno == errno.EEXIST):
                _dwarn(e.err)
            exit(e.errno)
    elif (o == '--start'):
        instence.start()
    elif (o == '--stop'):
        instence.stop()
    elif (o == '--stat'):
        stat = instence.show()
        for (k, v) in stat.items():
            _dwarn("%s:%s" % (k, v))
    elif (o == '--drop'):
        instence.drop()
    elif (o == '--disable'):
        instence.skip('1')
    elif (o == '--enable'):
        instence.skip('0')

        
if __name__ == '__main__':
    if (len(sys.argv) == 1):
        usage()
    else:
        main()
